/*
 * Copyright 2012 Walter Lütgenau.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package offset.nodes.client.editor.controller.component;

import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JSeparator;
import javax.swing.JToolBar.Separator;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.border.EtchedBorder;
import javax.swing.text.AttributeSet;
import javax.swing.text.ComponentView;
import javax.swing.text.Element;
import javax.swing.text.StyleConstants;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import javax.swing.text.html.HTML;
import offset.nodes.client.editor.Editor;
import offset.nodes.client.editor.controller.EditorKit;
import offset.nodes.client.editor.model.ComponentModel;
import offset.nodes.client.model.component.ComponentEditorEnvironment;
import offset.nodes.client.view.component.ComponentDialog;
import offset.nodes.client.view.component.ComponentEditor;

/**
 *
 * @author
 * Walter
 * Lütgenau
 */
public class ComponentKit implements ViewFactory, FocusListener {

    Editor editor;
    HashMap<Object,ComponentUI> allComponentsByPane = new HashMap<Object,ComponentUI>();
    HashMap<Object,ComponentUI> documentComponentsByUUID = new HashMap<Object,ComponentUI>();
    ComponentUI current;
    ComponentUI dialog;
    ComponentUI editorUI;
    
    Collection<ComponentFocusListener> componentFocusListeners = new ArrayList<ComponentFocusListener>();
    
    private Color nonFocusColor = new Color(245,245,245);
    private Color focusColor = Color.WHITE;
    private Border focusBorder = BorderFactory.createLoweredBevelBorder();
    private Border nonFocusBorder = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED); 
    
    /**
     * Information about a component.
     * 
     * Most of it is associated to its user interface, hence the name.
     */
    public class ComponentUI {
        Collection<JMenu> menus = new ArrayList<JMenu>();
        Collection<Component> toolbarButtons = new ArrayList<Component>();
        Container mainPaneContainer;
        Container treePaneContainer;
        PaneView view;
        JButton componentDialogButton;
        
        ComponentEditor component;
       
        /**
         * Create a component entry from the ComponentFrame.
         * 
         * This involves adding the menus and toolbar actions as well as the explorer pane
         * to the list of components.
         * 
         * @param component the component frame to be added
         */
        public ComponentUI(ComponentEditor component) {
            this.component = component;
            
            if (component != editor) {
                component.initComponentEmbedded();
            
                if (component instanceof ComponentDialog)
                    addComponentActions();
            }

            for (int i = 0; i < component.getMenuBar().getMenuCount(); i++) {
                JMenu menu = component.getMenuBar().getMenu(i);
                
                if (menu.isVisible()) {
                    menus.add(menu);
                    if (component != editor)
                        menu.setVisible(false);
                }
            }
            
            for (Component button : component.getToolBar().getComponents()) {
                if (button.isVisible()) {
                    toolbarButtons.add(button);
                    if (component != editor)
                        button.setVisible(false);
                }
            }

            mainPaneContainer = component.getMainPane().getParent();
            treePaneContainer = component.getTreePane().getParent();

            if (component != editor) {
                add(allComponentsByPane.get(editor.getMainPane()), getId());

                for (ComponentEditor childComponent : component.getComponentEditors()) {
                    addComponentEditor(childComponent);
                }
            } else
                editorUI = this;
        }
        
        /**
         * Add actions in menu bar and/or tool bar, that are specific 
         * for a component
         */
        protected void addComponentActions() {
            componentDialogButton = new JButton();
            componentDialogButton.setAction(new ComponentActions.EditAsDialog(this, editor));
            componentDialogButton.setToolTipText(java.util.ResourceBundle.getBundle("offset/nodes/client/editor/view/resources/EditorBundle").getString("component.frameView"));
            toolbarButtons.add(componentDialogButton);
            componentDialogButton.setVisible(false);

            Separator separator = new Separator();
            separator.setVisible(false);
            toolbarButtons.add(separator);

            JMenu pageMenu = new JMenu();
            pageMenu.setText(getEditor().getFileMenu().getText());

            for (Component component : getEditor().getFileMenu().getMenuComponents()) {
                if (component instanceof JMenuItem) {
                    JMenuItem editorMenuItem = (JMenuItem) component;

                    JMenuItem menuItem = new JMenuItem();
                    menuItem.setText(editorMenuItem.getText());
                    menuItem.setAction(editorMenuItem.getAction());

                    pageMenu.add(menuItem);
                } else if (component instanceof JSeparator)
                    pageMenu.add(new JSeparator());
            }

            // add page menu to menus as first menu
            Collection<JMenu> result = new ArrayList<JMenu>();
            result.add(pageMenu);
            for (JMenu menu : getMenus()) {
                result.add(menu);
            }
            getMenus().clear();
            for (JMenu menu : result) {
                getMenus().add(menu);
            }
        }

        /**
         * Return the first level menus of the component.
         * 
         * @return the menus.
         */
        public Collection<JMenu> getMenus() {
            return menus;
        }

        /**
         * Return the toolbar components..
         * 
         * @return the toolbar components.
         */
        public Collection<Component> getToolbarButtons() {
            return toolbarButtons;
        }

        /**
         * Return the component page editor.
         * 
         * @return the component page editor
         */
        public ComponentEditor getComponent() {
            return component;
        }

        /**
         * Return the main pane of the component.
         * 
         * @return the main pane
         */
        public JComponent getPane() {
            return getComponent().getMainPane();
        }
        
        /**
         * Return the explorer pane of the component.
         * 
         * @return the explorer pane.
         */
        public JComponent getTreePane() {
            return getComponent().getTreePane();
        }

        /**
         * Return the original parent of the main pane.
         * 
         * @return the container
         */
        public Container getMainPaneContainer() {
            return mainPaneContainer;
        }

        /**
         * Return the original parent of the tree pane.
         * 
         * @return the container
         */
        public Container getTreePaneContainer() {
            return treePaneContainer;
        }

        /**
         * Return the view hosting this component, if there is any.
         * 
         * @return the view, if hosted by a view, else null
         */
        public PaneView getView() {
            return view;
        }

        /**
         * Set the view 
         * 
         * @param view 
         */
        public void setView(PaneView view) {
            this.view = view;
        }
        
        /**
         * Show or hide the components actions.
         * 
         * This will include highlighting or unhighlighting the component.
         * 
         * @param show show or not show? This is here the question
         */
        protected void show(boolean show) {
            for (JMenu menu : getMenus()) {
                menu.setVisible(show);
            }

            for (Component toolbarButton : getToolbarButtons()) {
                toolbarButton.setVisible(show);
            }

            if (show) {
                getPane().setBackground(focusColor);
                if (getComponent() != editor)
                    getPane().setBorder(focusBorder);
            } else {
                getPane().setBackground(nonFocusColor);
                if (getComponent() != editor)
                    getPane().setBorder(nonFocusBorder);
            }

            if (show) {
                ((CardLayout) editor.getComponentTreePanels().getLayout()).show(editor.getComponentTreePanels(), getId());
                current = this;
                
                if (this != allComponentsByPane.get(editor.getMainPane()))
                    fireComponentFocusEvent(getView().getElement());
            }
        }
        
        public void prepareForEmbedded() {
            getComponent().initComponentEmbedded();
            componentDialogButton.setVisible(true);
            
            dialog = null;
            allComponentsByPane.get(editor.getMainPane()).show(false);
            setCurrent(this);
            
            add(allComponentsByPane.get(editor.getMainPane()), getId());
            view.simulate(false);
            
            getPane().setBorder(getFocusBorder());
         
        }
        
        public void prepareForDialog() {
            getComponent().initComponentDialog();
            componentDialogButton.setVisible(false);
            
            dialog = this;
            allComponentsByPane.get(editor.getMainPane()).show(true);
            
            add(this, "main");
            view.simulate(true);
            
            if (getPane().getParent() != getMainPaneContainer())
                getMainPaneContainer().add(getPane());         
            
            getPane().setBorder(new EmptyBorder(0, 0, 0, 0));
        }
        
        protected void add(ComponentUI componentUi, String id) {
            for (JMenu menu : getMenus())
                componentUi.getComponent().getMenuBar().add(menu);
            
            for (Component toolbarButton : getToolbarButtons())
                componentUi.getComponent().getToolBar().add(toolbarButton);
            
            if (getTreePane().getParent() != componentUi.getTreePaneContainer())
                componentUi.getTreePaneContainer().add(getTreePane(), id);

        }

        /**
         * Create a unique id for the component.
         *
         * @param component the component
         * @return the unique id.
         */
        protected String getId() {
            if (getComponent() == editor)
                return "main";
            return "" + hashCode();
        }
        
    }

    /**
     * Create a component kit.
     * 
     * @param editor the editor
     */
    public ComponentKit(Editor editor) {
        this.editor = editor;
        current = addComponentEditor(editor);
    }
    
    /**
     * Return the component frames managed by the kit.
     * 
     * @return the component frames.
     */
    public Collection<ComponentEditor> getComponentEditors() {
        Collection<ComponentEditor> result = new ArrayList<ComponentEditor>();
        
        for (ComponentUI componentUI : allComponentsByPane.values()) {
            if (componentUI.getComponent() != editor)
                result.add(componentUI.getComponent());
        }
        
        return result;
    }

    /**
     * Is the particular element associated to a component page?
     * 
     * @param e the element
     * @return the answer
     */
    public boolean isComponent(Element e) {
        AttributeSet attrs = e.getAttributes();
        return attrs != null
                && attrs.getAttribute(StyleConstants.NameAttribute) != null
                && attrs.getAttribute(StyleConstants.NameAttribute) == HTML.Tag.DIV
                && (attrs.isDefined(ComponentModel.ATT_COMPONENT_ID) || attrs.isDefined(ComponentModel.ATT_COMPONENT_PATH));
    }
    
    /**
     * Return the pane of an component element.
     * 
     * @param element the element
     * @return the pane
     */
    public JComponent getComponentPane(Element element) {
        if (!isComponent(element)
                || !documentComponentsByUUID.containsKey(element.getAttributes().getAttribute(ComponentModel.ATT_COMPONENT_ID)))
            return null;
        
        return documentComponentsByUUID.get(element.getAttributes().getAttribute(ComponentModel.ATT_COMPONENT_ID)).getPane();
    }

    /**
     * Create a component's view.
     * 
     * @param element the element to create the view for
     * @return the component's view
     */
    @Override
    public View create(Element element) {
        if (!isComponent(element))
            return null;

        ComponentUI componentUI;
        if (documentComponentsByUUID.containsKey(element.getAttributes().getAttribute(ComponentModel.ATT_COMPONENT_ID))) {
            componentUI = documentComponentsByUUID.get(element.getAttributes().getAttribute(ComponentModel.ATT_COMPONENT_ID));
        } else {
            ComponentEditor component = createAppletComponent(element);
            if (component == null)
                return createReadOnlyView(element);

            JComponent pane = component.getMainPane();

            pane.setPreferredSize(null);
            pane.setMinimumSize(null);
            pane.setMaximumSize(null);

            pane.setBackground(nonFocusColor);
            pane.setBorder(nonFocusBorder);
            
            componentUI = addComponentEditor(component);
            documentComponentsByUUID.put(element.getAttributes().getAttribute(ComponentModel.ATT_COMPONENT_ID), componentUI);
        }
        
        decoratePane(element, componentUI.getPane());
        PaneView paneView = new PaneView(element, componentUI.getPane()); 
        componentUI.setView(paneView) ;
        
        return paneView;
    }
    
    /**
     * Add a component focus listener
     * 
     * @param listener the listener
     */
    public void addComponentFocusListener(ComponentFocusListener listener) {
        componentFocusListeners.add(listener);
    }
    
    /**
     * Fire component focus events, that is, a component gained focus.
     * 
     * @param element the element, that gained the focus
     */
    public void fireComponentFocusEvent(Element element) {
        for (ComponentFocusListener listener : componentFocusListeners)
            listener.focusGained(element);
    }
    
    /**
     * Decorate the pane based on the element attributes. 
     * 
     * To be used in subclasses
     * 
     * @param element the element
     * @param pane the pane
     */
    protected void decoratePane(Element element, JComponent pane) {
    }
    
    /**
     * Add a component editor to the list of editors managed by the kit.
     * 
     * @param componentFrame
     * @param view
     * @return 
     */
    protected ComponentUI addComponentEditor(ComponentEditor componentEditor) {
        ComponentUI result = createComponentUI(componentEditor);
        allComponentsByPane.put(componentEditor.getMainPane(), result);
        componentEditor.getMainPane().addFocusListener(this);
        
        return result;
    }
    
    /**
     * Return the repository URL from the document URL.
     * 
     * @return the repository URL
     * @throws MalformedURLException 
     */
   protected URL getRepository() throws MalformedURLException {
        String document = (String) editor.getProperties().get(Editor.PROP_DOCUMENT_URL);
        String documentPath = (String) editor.getProperties().get(Editor.PROP_DOCUMENT_PATH);


        URL context = new URL(document);
        String path = null;
        if (documentPath != null && documentPath.length() > 0)
            path = context.getPath().substring(0, context.getPath().indexOf(documentPath));
        else
            path = context.getPath();
        context = new URL(context.getProtocol(), context.getHost(), context.getPort(), path);

        return context;
    }        

    /**
     * Create the applet component implement ComponentFrame from the information 
     * in the element.
     * 
     * Extract the uuid from the attributes, request the applet edit page from the server,
     * parse the page and create a simulated applet environment for the applet.
     * If the applet is a ComponentFrame, return it, else return null.
     * 
     * @param element
     * @return the initialized applet with the simulated environment.
     */
    protected ComponentEditor createAppletComponent(Element element) {
        try {
            URL repository = getRepository();
            URL dialog = new URL((String) editor.getProperties().get(Editor.PROP_UPLOAD_TO));
            String uuid = (String) element.getAttributes().getAttribute(ComponentModel.ATT_COMPONENT_ID);

            ComponentEditorEnvironment componentEditor = new ComponentEditorEnvironment(repository, dialog, uuid);
            componentEditor.init();
            if (componentEditor.getComponent() == null 
                    || !(componentEditor.getComponent() instanceof ComponentEditor))
                return null;
            return (ComponentEditor) componentEditor.getComponent();
        } catch (MalformedURLException ex) {
            Logger.getLogger(ComponentKit.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
    }
    
    /**
     * Represents the uuid an editable page?
     * 
     * @param uuid 
     * @return 
     */
    public boolean isEditablePage(String uuid) {
        try {
            URL repository = getRepository();
            URL dialog = new URL((String) editor.getProperties().get(Editor.PROP_UPLOAD_TO));

            ComponentEditorEnvironment componentEditor = new ComponentEditorEnvironment(repository, dialog, uuid);
            return componentEditor.isEditablePage();
        } catch (MalformedURLException ex) {
            Logger.getLogger(ComponentKit.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        }

    }
    
    /**
     * In the future, displaying of non-editable components will be supported.
     * 
     * @param element the element representing a non-editable component
     * @return a view representing the element
     */
    protected View createReadOnlyView(Element element) {
        try {
            JEditorPane pane = new JEditorPane();
            pane.setEditable(false);
            pane.setPage(new URL("Not yet supported"));
            return new PaneView(element, pane);
        } catch (Exception ex) {
            Logger.getLogger(ComponentKit.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
    }

    /**
     * Only used for testing.
     * 
     * @param element
     * @return 
     */
    protected ComponentEditor createTestComponent(Element element) {
        Editor newEditor = new Editor();

        try {
            newEditor.init();
        } catch (Throwable e) {
        }
        newEditor.start();
        try {
            newEditor.setMode(Editor.MODE_LOCAL_PAGE_EDITOR);
            newEditor.open(new URL("file:/D:/component.html"));
        } catch (Exception ex) {
            Logger.getLogger(EditorKit.class.getName()).log(Level.SEVERE, null, ex);
        }


        return newEditor;
    }
    
    /**
     * Create a new component UI. 
     * 
     * This can be used in derived classes to overwrite the default component UI.
     * 
     * @param componentEditor the component editor decorated by the component UI
     * @return the component UI.
     */
    protected ComponentUI createComponentUI(ComponentEditor componentEditor) {
        return new ComponentUI(componentEditor);
    }
    
    protected String printObject(Object o) {
        if (o == null)
            return "(null)";
        else
            return "" + o.hashCode();
    }
    
    int ignoreFocusEvents = 0;
    
    /**
     * The component has gained focus.
     * 
     * @param e 
     */
    @Override
    public void focusGained(FocusEvent e) {
        if (allComponentsByPane.get(e.getSource()) == null // not a component pane
                || e.getOppositeComponent() == null
                || (dialog != null && dialog == allComponentsByPane.get(e.getSource()))) // external focus change
            return;
        
        if (current != null)
            current.show(false);

        if (allComponentsByPane.containsKey(e.getSource()))
            allComponentsByPane.get(e.getSource()).show(true);
    }

    /**
     * The component has lost focus.
     * 
     * @param e 
     */
    @Override
    public void focusLost(FocusEvent e) {
        if (allComponentsByPane.get(e.getSource()) == null // not a component pane
                || e.getOppositeComponent() == null // external focus change
                || allComponentsByPane.get(e.getOppositeComponent()) == null) // focus to non pane
            return;

        if (current != null)
            current.show(false);
        if (allComponentsByPane.containsKey(e.getOppositeComponent()))
            allComponentsByPane.get(e.getOppositeComponent()).show(true);
    }
    
    
    /**
     * For component panes like JEditorPane, that will try to get all available space in the Y direction, this view will limit the
     * maximum Y size to the preferred Y size.
     */
    class PaneView extends ComponentView {
        class ComponentSwitcher extends Container {
            JComponent component;
            
            public ComponentSwitcher(JComponent component) {
                this.component = component;
                setLayout(null);
            }
            
            public void simulate(boolean simulate) {
                JComponent child;
                if (simulate) {
                    Dimension size = component.getSize();
                    Image image = component.createImage(size.width, size.height);
                    component.paint(image.getGraphics());
                    JComponent simulator = new JLabel(new ImageIcon(image));
                    
                    child = simulator;
                } else {
                    child = component;
                }
                
                if (getComponentCount() > 0)
                    remove(getComponent(0));
                add(child);
            }

            @Override
            public Dimension getPreferredSize() {
                return getComponent(0).getPreferredSize();
            }

            @Override
            public Dimension getMinimumSize() {
                return getComponent(0).getMinimumSize();
            }

            @Override
            public Dimension getMaximumSize() {
                return getComponent(0).getMaximumSize();
            }

            @Override
            public float getAlignmentX() {
                return getComponent(0).getAlignmentX();
            }

            @Override
            public float getAlignmentY() {
                return getComponent(0).getAlignmentY();
            }
            
            @Override
            public void setBounds(int x, int y, int w, int h) {
                super.setBounds(x, y, w, h);
                if (getComponentCount() > 0)
                    getComponent(0).setSize(w, h);
            }

        }
        
        float previousSpan = -1f;
        Container pane;
        boolean simulated = false;

        public PaneView(Element elem, JComponent realPane) {
            super(elem);
            this.pane = new ComponentSwitcher(realPane);
        }
        
        /**
         * Return the preferred size for Y_AXIS, else default behaviour.
         * Note, that if the Y size changes, the view hierarchy must be invalidated.
         * 
         * @param axis
         * @return 
         */
        public float getMaximumSpan(int axis) {  
            if (axis == Y_AXIS) {
                float result = getPreferredSpan(axis);
                if (previousSpan >= 0 
                        && result != previousSpan) {
                    Runnable invalidate = new Runnable() {
                        public void run() {
                            Container container = getContainer();
                            if (container != null) {
                                preferenceChanged(null, true, true);
                                container.repaint();
                            }
                        }
                    };
                    SwingUtilities.invokeLater(invalidate);
                }
                previousSpan = result;

                return result;
            }
            
            return super.getMaximumSpan(axis);
        }
        
        /**
         * Return the applet component.
         * 
         * @return the component
         */
        protected Component createComponent() {
            simulate(false);
            return pane;
        }
        
        public void simulate(boolean simulate) {
            ((ComponentSwitcher)pane).simulate(simulate);
        }
    }    

    public Color getNonFocusColor() {
        return nonFocusColor;
    }

    public void setNonFocusColor(Color nonFocusColor) {
        this.nonFocusColor = nonFocusColor;
    }

    public Color getFocusColor() {
        return focusColor;
    }

    public void setFocusColor(Color focusColor) {
        this.focusColor = focusColor;
    }

    public Border getFocusBorder() {
        return focusBorder;
    }

    public void setFocusBorder(Border focusBorder) {
        this.focusBorder = focusBorder;
    }

    public Border getNonFocusBorder() {
        return nonFocusBorder;
    }

    public void setNonFocusBorder(Border nonFocusBorder) {
        this.nonFocusBorder = nonFocusBorder;
    }

    public Editor getEditor() {
        return editor;
    }

    public ComponentUI getCurrent() {
        return current;
    }

    public void setCurrent(ComponentUI current) {
        this.current = current;
    }

    public ComponentUI getEditorUI() {
        return editorUI;
    }

    public void setEditorUI(ComponentUI editorUI) {
        this.editorUI = editorUI;
    }

}
